/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.imagepipeline.animated.impl;

import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod;
import com.facebook.imagepipeline.animated.base.AnimatedImage;
import com.facebook.imagepipeline.animated.base.AnimatedImageResult;
import com.facebook.imagepipeline.transformation.BitmapTransformation;
import com.oszc.bbhmlibrary.utils.CommonUtils;
import com.oszc.bbhmlibrary.utils.LogUtil;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.agp.utils.RectFloat;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;

/**
 * Contains the logic for compositing the frames of an {@link AnimatedImage}. Animated image formats
 * like GIF and WebP support inter-frame compression where a subsequent frame may require being
 * blended on a previous frame in order to render the full frame. This class encapsulates the
 * behavior to be able to render any frame of the image. Designed to work with a cache via a
 * Callback.
 */
public class AnimatedImageCompositor {

  private final String TAG = AnimatedImageCompositor.class.getSimpleName();
  /** Callback for caching. */
  public interface Callback {

    /**
     * Called from within {@link #renderFrame} to let the caller know that while trying generate the
     * requested frame, an earlier frame was generated. This allows the caller to optionally cache
     * the intermediate result. The caller must copy the PixelMap if it wishes to cache it as {@link
     * #renderFrame} will continue using it generate the requested frame.
     *
     * @param frameNumber the frame number of the intermediate result
     * @param bitmap the bitmap which must not be modified or directly cached
     */
    void onIntermediateResult(int frameNumber, PixelMap bitmap);

    /**
     * Called from within {@link #renderFrame} to ask the caller for a cached bitmap for the
     * specified frame number. If the caller has the bitmap cached, it can greatly reduce the work
     * required to render the requested frame.
     *
     * @param frameNumber the frame number to get
     * @return a reference to the bitmap. The ownership of the reference is passed to the caller who
     *     must close it.
     */
    CloseableReference<PixelMap> getCachedBitmap(int frameNumber);
  }

  private final AnimatedDrawableBackend mAnimatedDrawableBackend;
  private final Callback mCallback;
  private final Paint mTransparentFillPaint;
  private final Paint mNormalPaint;

  public AnimatedImageCompositor(
      AnimatedDrawableBackend animatedDrawableBackend, Callback callback) {
    mAnimatedDrawableBackend = animatedDrawableBackend;
    mCallback = callback;
    mTransparentFillPaint = new Paint();
    mTransparentFillPaint.setColor(Color.TRANSPARENT);
    mTransparentFillPaint.setStyle(Paint.Style.FILL_STYLE);

    //mTransparentFillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
    mTransparentFillPaint.setBlendMode(BlendMode.SRC);

    mNormalPaint = new Paint();
    mNormalPaint.setAntiAlias(true);
    mNormalPaint.setDither(true);
    mNormalPaint.setBlendMode(BlendMode.SRC);
  }

  /**
   * Renders the specified frame. Only should be called on the rendering thread.
   *
   * @param frameNumber the frame to render
   * @param bitmap the bitmap to render into
   */
  public void renderFrame(int frameNumber, PixelMap bitmap) {
    LogUtil.error(TAG,"canvas renderFrame frameNumber:"+frameNumber+" size:"+bitmap.getImageInfo().size+"   format:"+bitmap.getImageInfo().pixelFormat.name());

    bitmap.resetConfig(bitmap.getImageInfo().size, PixelFormat.RGB_565);
    Texture texture = new Texture(bitmap);
    LogUtil.error(TAG,"canvas renderFrame origin bitmap:"+bitmap+"   texture bitmap:"+texture.getPixelMap());
    Canvas canvas = new Canvas(texture);
    canvas.drawColor(Color.TRANSPARENT.getValue(), Canvas.PorterDuffMode.SRC);
    //canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC);

    // If blending is required, prepare the canvas with the nearest cached frame.
    int nextIndex;
    if (!isKeyFrame(frameNumber)) {
      // Blending is required. nextIndex points to the next index to render onto the canvas.
      nextIndex = prepareCanvasWithClosestCachedFrame(frameNumber - 1, canvas);
    } else {
      // Blending isn't required. Start at the frame we're trying to render.
      nextIndex = frameNumber;
    }

    // Iterate from nextIndex to the frame number just preceding the one we're trying to render
    // and composite them in order according to the Disposal Method.
    for (int index = nextIndex; index < frameNumber; index++) {
      AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
      DisposalMethod disposalMethod = frameInfo.disposalMethod;
      if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) {
        continue;
      }
      if (frameInfo.blendOperation == BlendOperation.NO_BLEND) {
        disposeToBackground(canvas, frameInfo);
      }
      mAnimatedDrawableBackend.renderFrame(index, canvas);
      mCallback.onIntermediateResult(index, bitmap);
      if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) {
        disposeToBackground(canvas, frameInfo);
      }
    }

    AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(frameNumber);
    if (frameInfo.blendOperation == BlendOperation.NO_BLEND) {
      disposeToBackground(canvas, frameInfo);
    }
    // Finally, we render the current frame. We don't dispose it.
    mAnimatedDrawableBackend.renderFrame(frameNumber, canvas);
    maybeApplyTransformation(bitmap);
  }

  /** Return value for {@link #isFrameNeededForRendering} used in the compositing logic. */
  private enum FrameNeededResult {
    /** The frame is required to render the next frame */
    REQUIRED,

    /** The frame is not required to render the next frame. */
    NOT_REQUIRED,

    /** Skip this frame and keep going. Used for GIF's DISPOSE_TO_PREVIOUS */
    SKIP,

    /** Stop processing at this frame. This means the image didn't specify the disposal method */
    ABORT
  }

  /**
   * Given a frame number, prepares the canvas to render based on the nearest cached frame at or
   * before the frame. On return the canvas will be prepared as if the nearest cached frame had been
   * rendered and disposed. The returned index is the next frame that needs to be composited onto
   * the canvas.
   *
   * @param previousFrameNumber the frame number that is ones less than the one we're rendering
   * @param canvas the canvas to prepare
   * @return the index of the the next frame to process
   */
  private int prepareCanvasWithClosestCachedFrame(int previousFrameNumber, Canvas canvas) {
    LogUtil.error(TAG,"canvas prepareCanvasWithClosestCachedFrame ");
    for (int index = previousFrameNumber; index >= 0; index--) {
      FrameNeededResult neededResult = isFrameNeededForRendering(index);
      switch (neededResult) {
        case REQUIRED:
          AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
          CloseableReference<PixelMap> startBitmap = mCallback.getCachedBitmap(index);
          if (startBitmap != null) {
            try {
              //canvas.drawBitmap(startBitmap.get(), 0, 0, null);
              canvas.drawPixelMapHolder(new PixelMapHolder(startBitmap.get()),0,0,mNormalPaint);
              if (frameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) {
                disposeToBackground(canvas, frameInfo);
              }
              return index + 1;
            } finally {
              startBitmap.close();
            }
          } else {
            if (isKeyFrame(index)) {
              return index;
            } else {
              // Keep going.
              break;
            }
          }
        case NOT_REQUIRED:
          return index + 1;
        case ABORT:
          return index;
        case SKIP:
        default:
          // Keep going.
      }
    }
    return 0;
  }

  private RectFloat mTempRect = new RectFloat();
  private void disposeToBackground(Canvas canvas, AnimatedDrawableFrameInfo frameInfo) {
    mTempRect.left = frameInfo.xOffset;
    mTempRect.top = frameInfo.yOffset;
    mTempRect.right = frameInfo.xOffset + frameInfo.width;
    mTempRect.bottom = frameInfo.yOffset + frameInfo.height;
    canvas.drawRect(mTempRect,mTransparentFillPaint);
  }

  /**
   * Returns whether the specified frame is needed for rendering the next frame. This is part of the
   * compositing logic. See {@link FrameNeededResult} for more info about the results.
   *
   * @param index the frame to check
   * @return whether the frame is required taking into account special conditions
   */
  private FrameNeededResult isFrameNeededForRendering(int index) {
    AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
    DisposalMethod disposalMethod = frameInfo.disposalMethod;
    if (disposalMethod == DisposalMethod.DISPOSE_DO_NOT) {
      // Need this frame so keep going.
      return FrameNeededResult.REQUIRED;
    } else if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) {
      if (isFullFrame(frameInfo)) {
        // The frame covered the whole image and we're disposing to background,
        // so we don't even need to draw this frame.
        return FrameNeededResult.NOT_REQUIRED;
      } else {
        // We need to draw the image. Then erase the part the previous frame covered.
        // So keep going.
        return FrameNeededResult.REQUIRED;
      }
    } else if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) {
      return FrameNeededResult.SKIP;
    } else {
      return FrameNeededResult.ABORT;
    }
  }

  private boolean isKeyFrame(int index) {
    if (index == 0) {
      return true;
    }
    AnimatedDrawableFrameInfo currFrameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
    AnimatedDrawableFrameInfo prevFrameInfo = mAnimatedDrawableBackend.getFrameInfo(index - 1);
    if (currFrameInfo.blendOperation == BlendOperation.NO_BLEND && isFullFrame(currFrameInfo)) {
      return true;
    } else
      return prevFrameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND
          && isFullFrame(prevFrameInfo);
  }

  private boolean isFullFrame(AnimatedDrawableFrameInfo frameInfo) {
    return frameInfo.xOffset == 0
        && frameInfo.yOffset == 0
        && frameInfo.width == mAnimatedDrawableBackend.getRenderedWidth()
        && frameInfo.height == mAnimatedDrawableBackend.getRenderedHeight();
  }

  private void maybeApplyTransformation(PixelMap bitmap) {
    AnimatedImageResult animatedImageResult = mAnimatedDrawableBackend.getAnimatedImageResult();

    if (animatedImageResult == null) {
      return;
    }

    BitmapTransformation tr = animatedImageResult.getBitmapTransformation();

    if (tr == null) {
      return;
    }

    tr.transform(bitmap);
  }
}
